import { debug, Jest } from "./buildConfig.mts";
import type { BRule, Rule } from "./types.d.mts";

export const /** 元素规则 */
  CRRE =
    /^(\[\$domain=)?(~?[\w-]+(?:(?:\.[\w-]+)*\.(?:[\w-]+|\*))?(?:[,|]~?[\w-]+(?:(?:\.[\w-]+)*\.(?:[\w-]+|\*))?)*)?]?(#@?\$?\??#)([^\s+@^][^@]*(?:["'([]+.*["')\]]+)*[^@]*)\s*$/,
  /** 基本规则 */
  BRRE =
    /^@@?(?:\/(.*[^\\])\/|(\|\|?)?(https?:\/\/)?([^\s"<>`]+?[^|]?))?\$((?:(?:~?[\w-]+(?:=[^$]+)?|_+)(?:[^\\],|$))+)/,
  /** 预存 CSS */
  CCRE =
    /^\/\*\s(\d)(\|)?(.+?)\s\*\/\s((.+?)\s*(?:{\s*[A-Za-z-]+\s*:\s*.+}|,))\s*$/,
  /** 预存注释 */
  CMRE = /\/\*\s*\d.+?\s*\*\//g,
  /** CSS 选择器 */
  CSRE = /^(.+?)\s*{\s*[A-Za-z-]+\s*:\s*.+}\s*$/,
  BROptions = [
    "elemhide",
    "ehide",
    "specifichide",
    "shide",
    "generichide",
    "ghide",
  ] as const,
  CRFlags = [
    "##",
    "#@#",
    "#?#",
    "#@?#",
    "#$#",
    "#@$#",
    "#$?#",
    "#@$?#",
  ] as const,
  styleBoxes = [
    "genHideCss",
    "genExtraCss",
    "spcHideCss",
    "spcExtraCss",
  ] as const;

/**
 * 处理 禁用元素隐藏规则
 * @param rule 禁用元素隐藏规则
 * @returns 失败返回 null，成功返回 { rule: BRule; bad: boolean }
 */
export function bRuleSpliter(
  rule: string,
): { rule: BRule; bad: boolean } | null {
  const group = BRRE.exec(rule);
  if (!group) {
    return null;
  }
  const [, regex, pipe, proto, body, option] = group,
    options = option.split(","),
    separatorChar = String.raw`[^\w\.%-]`,
    anyChar = '(?:[^\\s"<>`]*)',
    eh = hasSome(options, ["elemhide", "ehide"]),
    sh = hasSome(options, ["specifichide", "shide"]),
    gh = hasSome(options, ["generichide", "ghide"]);
  let domains: string[] = [];
  for (const opt of options) {
    if (opt.startsWith("domain=")) {
      domains = opt.slice(7).split("|");
    }
  }
  let match: string | string[] = "";
  if (debug && Jest) {
    console.info(pipe, proto, body, regex);
  }
  if (regex) {
    match = regex;
  } else if (body) {
    match += pipe
      ? proto
        ? `^${proto}`
        : `^https?://(?:[\\w-]+\\.)*?`
      : `^${anyChar}`;
    match += body
      .replaceAll(/[$()+.[\\\]{}-]/g, String.raw`\$&`)
      .replaceAll("^", "$^")
      .replace(/\|$/, "$")
      .replaceAll("|", String.raw`\|`)
      .replace(/\*$/, "")
      .replaceAll("*", anyChar)
      .replace(/\$\^$/, `(?:${separatorChar}.*|$)`)
      .replaceAll("$^", separatorChar);
  } else if (domains.length > 0) {
    match = domains;
  }

  return {
    rule: {
      rule,
      match,
      level: eh || (gh && sh) ? 3 : sh ? 2 : gh ? 1 : 0,
    },
    bad: options.includes("badfilter"),
  };
}

/**
 * 判断是否为禁用元素隐藏规则
 * @param {string} rule ABP 规则
 * @returns {boolean} 判断结果
 */
export function isBasicRule(rule: string): boolean {
  return BRRE.test(rule) && hasSome(rule, BROptions);
}

/**
 * 检查 BRule 对象是否匹配应用地址
 * @param {?BRule} rule BRule 对象
 * @param {string=} url 应用地址
 * @returns {number} 应用级别，不匹配返回 0
 */
export function bRuleParser(
  rule?: BRule,
  url: string = location.href,
): BRule["level"] {
  if (debug && Jest) {
    console.info(
      `${
        Array.isArray(rule?.match) ? rule.match.toString() : (rule?.match ?? "")
      }\n${url}`,
    );
  }
  return rule
    ? (Array.isArray(rule.match) && domainChecker(rule.match)[0]) ||
      (!Array.isArray(rule.match) && new RegExp(rule.match).test(url))
      ? rule.level
      : 0
    : 0;
}

/**
 * 裁剪提取 ETag
 * @param {?string} header 请求头中的 ETag 属性字符串
 * @returns {?string} ETag 属性字符串，未找到返回 null
 */
export function getEtag(header?: string): string | null {
  let result: RegExpMatchArray | null = null;
  if (!header) {
    return null;
  }
  for (const re of [
    /[Ee|][Tt|]ag[:=]\s?\[?(?:W\/)?"(?:gz\[)?(\w+)]?"]?/,
    // 海阔世界
    /^(?:W\/)?"(?:gz\[)?(\w+)]?"/,
  ]) {
    result ??= header.match(re);
  }
  return result?.[1] ?? null;
}

/**
 * 检查 ABP 域名范围是否匹配当前域名
 * @param {string[]} domains 一组 ABP 域名
 * @param {string=} currDomain 当前域名
 * @returns {boolean[]} [ 是否匹配, 是否是通用规则 ]
 */
export function domainChecker(
  domains: string[],
  currentDomain: string = location.hostname,
): [boolean, boolean] {
  type Result = [number, boolean];
  const results: Result[] = [],
    invResults: Result[] = [],
    urlSuffix = /\.+?[\w-]+$/.exec(currentDomain)?.[0];
  let totalResult: Result = [0, false],
    black = false,
    white = false,
    match = false;
  for (let domain of domains) {
    const invert = domain.startsWith("~");
    if (invert) {
      domain = domain.slice(1);
    }
    if (domain.endsWith(".*") && urlSuffix) {
      domain = domain.replace(".*", urlSuffix);
    }
    const result = currentDomain.endsWith(domain);
    if (invert) {
      if (result) {
        white = true;
      }
      invResults.push([domain.length, !result]);
    } else {
      if (result) {
        black = true;
      }
      results.push([domain.length, result]);
    }
  }
  if (results.length > 0 && !black) {
    match = false;
  } else if (invResults.length > 0 && !white) {
    match = true;
  } else {
    for (const r of results) {
      if (r[0] >= totalResult[0] && r[1]) {
        totalResult = r;
      }
    }
    for (const r of invResults) {
      if (r[0] >= totalResult[0] && !r[1]) {
        totalResult = r;
      }
    }
    match = totalResult[1];
  }
  return [match, results.length === 0];
}

/**
 * 检查“句子”内容或“给定单词”中是否存在任一“匹配单词”
 * @param {(string | string[])} str 一个“句子”或一组“给定单词”
 * @param {string[]} arr 一组“匹配单词”
 * @returns {boolean} 结果
 */
export function hasSome(
  string_: string | string[],
  array: readonly string[],
): boolean {
  return array.some((word) => string_.includes(word));
}

/**
 * 处理 ABP 元素隐藏规则
 * @param {string} rule ABP 元素隐藏规则
 * @returns {(Rule | undefined)} Rule 对象，失败返回 undefined
 */
export function ruleLoader(rule: string): Rule | undefined {
  if (
    hasSome(rule, [
      ":matches-path(",
      ":min-text-length(",
      ":watch-attr(",
      ":-abp-properties(",
      ":matches-property(",
    ])
  ) {
    return;
  }
  rule = rule.trim();
  // 如果 #$# 不包含 {} 就排除
  // 可以尽量排除 Snippet Filters
  if (
    /(?:\w|\*|]|^)#\$#/.test(rule) &&
    !/{\s*[A-Za-z-]+\s*:\s*.+}\s*$/.test(rule)
  ) {
    return;
  }
  // ## -> #?#
  if (
    /(?:\w|\*|]|^)#@?\$?#/.test(rule) &&
    hasSome(rule, [
      ":has(",
      ":-abp-has(",
      "[-ext-has=",
      ":has-text(",
      ":contains(",
      ":-abp-contains(",
      "[-ext-contains=",
      ":matches-css(",
      "[-ext-matches-css=",
      ":matches-css-before(",
      "[-ext-matches-css-before=",
      ":matches-css-after(",
      "[-ext-matches-css-after=",
      ":matches-attr(",
      ":nth-ancestor(",
      ":upward(",
      ":xpath(",
      ":remove()",
      ":not(",
    ])
  ) {
    rule = rule.replace(/(\w|\*|]|^)#(@?\$?)#/, "$1#$2?#");
  }
  // :style(...) 转换
  // example.com#?##id:style(color: red)
  // example.com#$?##id { color: red }
  if (rule.includes(":style(")) {
    rule = rule
      .replace(/(\w|\*|]|^)#(@?)(\??)#/, "$1#$2$$$3#")
      .replace(/:style\(\s*/, " { ")
      .replace(/\s*\)$/, " }");
  }
  // 解构
  const group = CRRE.exec(rule);
  if (group) {
    const [, isDomain, place = "*", flag, sel] = group,
      type = CRFlags.indexOf(flag as (typeof CRFlags)[number]),
      [match, generic] =
        place === "*"
          ? [true, true]
          : domainChecker(place.split(isDomain ? "|" : ","));
    if (sel && match) {
      return {
        black: type % 2 ? "white" : "black",
        type: Math.floor(type / 2) as 0 | 1 | 2 | 3,
        place: (isDomain ? "|" : "") + place,
        generic,
        sel,
      };
    }
  }
}

/**
 * 转换 Rule 对象为 CSS 规则或选择器
 * @param {Rule} rule Rule 对象
 * @param {string} preset 默认 CSS 声明，需要带 {}
 * @param {boolean} full css 值，`true` CSS 规则，`false` 选择器带逗号
 * @returns 返回如下对象
 * ```ts
 * type cssO = {
 *   // CSS 规则或选择器
 *   css: string;
 *   // 选择器
 *   sel: string;
 *   // Rule 对象中是否包含 CSS 声明
 *   isStyle: boolean;
 * }
 * ```
 */
export function ruleToCss(
  rule: Rule,
  preset: string,
  full: boolean,
): { css: string; sel: string; isStyle: boolean } {
  const isStyle = /}\s*$/.test(rule.sel);

  return {
    css: `/* ${String(rule.type)}${rule.place} */ ${
      rule.sel +
      (isStyle
        ? ""
        : full
          ? " " + preset.replaceAll(/^\s{2,}/g, " ").replaceAll("\n", "")
          : ",")
    } \n`,
    sel: isStyle ? (CSRE.exec(rule.sel)?.[1] ?? rule.sel) : rule.sel,
    isStyle,
  };
}

/**
 * 转换 CSS 规则为 ABP 规则 AdGuard 格式
 * @param {string} css CSS 规则
 * @returns 返回如下对象，失败返回 null
 * ```ts
 * type abpO = {
 *   // ABP 规则
 *   abp: string;
 *   // 选择器
 *   sel: string;
 *   // 规则类型
 *   type: 0 | 1 | 2 | 3;
 * }
 * ```
 */
export function cssToAbp(
  css: string,
): { abp: string; type: Rule["type"]; sel: string } | null {
  const flags = ["##", "#?#", "#$#", "#$?#"];
  const [, typeString, isDomain, place, style, sel] = CCRE.exec(css) ?? [];
  if (typeString === void 0) {
    return null;
  }
  const type = Number.parseInt(typeString) as Rule["type"];
  return {
    abp: `${place === "*" ? "" : isDomain ? `[$domain=${place}]` : place}${
      flags[type]
    }${type >= 2 ? style : sel}`,
    type,
    sel,
  };
}

/**
 * 给 URL 添加时间戳，防止缓存
 * @see https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest_API/Using_XMLHttpRequest#%E7%BB%95%E8%BF%87%E7%BC%93%E5%AD%98
 * @param {string} url 原始 URL
 * @returns {string} 处理后的 URL
 */
export function addTimeParameter(url: string): string {
  return url + (url.includes("?") ? "&" : "?") + String(Date.now());
}
